package va.radiomaria.p2p;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.Vector;
import javax.media.ControllerListener;

import va.radiomaria.audio.AudioPlayer;
import va.radiomaria.audio.DefaultAudioPlayer;
import va.radiomaria.audio.LikeCounter;
import va.radiomaria.audio.Mood;
import va.radiomaria.audio.Song;
import va.radiomaria.gui.DataModel;
import va.radiomaria.gui.LoggerInstance;
import net.tomp2p.futures.FutureBootstrap;
import net.tomp2p.futures.FutureDHT;
import net.tomp2p.p2p.Peer;
import net.tomp2p.p2p.PeerMaker;
import net.tomp2p.p2p.config.ConfigurationGet;
import net.tomp2p.p2p.config.ConfigurationRemove;
import net.tomp2p.p2p.config.ConfigurationStore;
import net.tomp2p.p2p.config.Configurations;
import net.tomp2p.peers.Number160;
import net.tomp2p.storage.Data;

/**
 * Contains all functionality that a peer needs on top of the DHT peer
 * functionality.
 * 
 */
public class RadioPeer {

	protected final int id;
	protected final Peer peer; // TomP2P peer
	protected AudioPlayer audioPlayer;
	protected ConfigurationStore cs = Configurations
			.defaultStoreConfiguration();
	protected ConfigurationRemove cr = Configurations
			.defaultRemoveConfiguration();
	protected ConfigurationGet getconfig = Configurations
			.defaultGetConfiguration();
	protected boolean isActive = false;
	protected Map<Mood, Vector<String>> moodToGenreMapping = new HashMap<Mood, Vector<String>>();
	protected DataModel model = null;
	protected Vector<String> playedSongs = new Vector<String>(); // keeps track
																	// of played
																	// songs

	/**
	 * Default constructor for already generated TomP2P peers
	 * 
	 * @param id
	 *            Peer id
	 * @param peer
	 *            TomP2P Peer object
	 */
	public RadioPeer(int id, Peer peer) {
		// initialize radio peers
		this.id = id;
		this.peer = peer;
		this.audioPlayer = new DefaultAudioPlayer();

		// Initialize genres for moods
		addGenreForMood("pop", Mood.PARTY);
		addGenreForMood("house", Mood.PARTY);
		addGenreForMood("pop", Mood.ROMANTIC);
		addGenreForMood("classical", Mood.ROMANTIC);
		addGenreForMood("house", Mood.WORKOUT);
		addGenreForMood("metal", Mood.WORKOUT);
	}

	/**
	 * Constructor that generates a new TomP2P Peer based on the ID supplied
	 * 
	 * @param id
	 * @throws IOException
	 */
	public RadioPeer(int id) throws IOException {
		this(4000 + (id % 1000), new PeerMaker(Number160.createHash(id))
				.setPorts(4000 + (id % 1000)).buildAndListen());
	}

	/**
	 * Adds a mood genre pair to the mapping of moods to genres
	 * 
	 * @param genre
	 * @param mood
	 */
	public void addGenreForMood(String genre, Mood mood) {
		if (!moodToGenreMapping.containsKey(mood)) {
			Vector<String> genres = new Vector<String>();
			genres.add(genre);
			moodToGenreMapping.put(mood, genres);
		} else {
			moodToGenreMapping.get(mood).add(genre);
		}

	}

	/**
	 * Bootstraps to localhost at the port supplied.
	 * 
	 * @param port
	 */
	public void bootstrapKnownPeer(int port) {
		bootstrapKnownPeer(new InetSocketAddress("127.0.0.1", port));
	}

	/**
	 * Bootstraps to a bootstrap service listening at the supplied
	 * inetSocketAddress
	 * 
	 * @param inetSocketAddress
	 */
	public void bootstrapKnownPeer(InetSocketAddress inetSocketAddress) {
		try {
			logMessage("Bootstrapping with well known peer "
					+ inetSocketAddress.getHostName() + " port: "
					+ inetSocketAddress.getPort(), LoggerInstance.P2P);
			FutureBootstrap fb = peer.bootstrap(inetSocketAddress);
			fb.awaitUninterruptibly();
			peer.discover(fb.getBootstrapTo().iterator().next())
					.awaitUninterruptibly();
			logMessage("Bootstrap successful", LoggerInstance.P2P);
		} catch (Exception e) {
			logMessage(
					"Failed to bootstrap. Check well known peer address and port",
					LoggerInstance.P2P);
		}

	}

	/**
	 * Adds the songs information to the DHT
	 * 
	 * @param song
	 */
	public void addSong(Song song) {
		Number160 genreHash = Number160.createHash(song.getGenre());
		try {
			Number160 contentKey = Number160.createHash(song.getUrl());
			HashMap<Number160, Data> contentMap = new HashMap<Number160, Data>();
			contentMap.put(contentKey, new Data(song));
			cs.setDomain(Number160.createHash("songs"));
			logMessage("Putting song: \'" + song.getSongTitle()
					+ "\' location key: \'" + song.getGenre()
					+ "\' content key: \'" + contentKey + "\'",
					LoggerInstance.P2P);
			FutureDHT future = peer.put(genreHash, contentMap, cs);
			future.awaitUninterruptibly();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	/**
	 * Returns all songs stored in the DHT for a given genre
	 * 
	 * @param genre
	 * @return
	 */
	public Collection<Data> getSongsForGenre(String genre) {
		Number160 genreHash = Number160.createHash(genre);
		getconfig.setDomain(Number160.createHash("songs"));
		FutureDHT songsFuture = peer.getAll(genreHash, getconfig);
		songsFuture.awaitUninterruptibly();
		if (songsFuture.isSuccess()) {
			return songsFuture.getDataMap().values();
		} else {
			return null;
		}
	}

	/**
	 * Uses the recommender system to choose which song to play for a given mood
	 * and plays it in the audio player module.
	 * 
	 * @param mood
	 * @return Song that the recommender selected for playing
	 * 
	 * @throws UnsupportedMoodException
	 * @throws NoSongsFoundException
	 * @throws IOException
	 */
	public Song playBestSongForMood(Mood mood) throws UnsupportedMoodException,
			NoSongsFoundException, IOException {
		logMessage("Looking up songs for mood: " + mood, LoggerInstance.P2P);
		if (moodToGenreMapping.containsKey(mood)) {
			Vector<Data> songsForMood = new Vector<Data>();
			for (String genre : moodToGenreMapping.get(mood)) {
				Collection<Data> songsForGenre = getSongsForGenre(genre);
				if (songsForGenre != null) {
					logMessage(" -> found " + songsForGenre.size()
							+ " songs in genre: " + genre, LoggerInstance.P2P);
					songsForMood.addAll(songsForGenre);
				} else {
					logMessage(" -> found 0 songs in genre: " + genre,
							LoggerInstance.P2P);
				}
			}
			if (songsForMood.size() == 0) {
				throw new NoSongsFoundException();
			}
			Random rand = new Random();
			HashMap<Song, Integer> songs = new HashMap<Song, Integer>();
			int randomSongsCount = Math.min(4, songsForMood.size());
			logMessage("Selecting " + randomSongsCount + "random Songs",
					LoggerInstance.RECOMMENDER);
			while (songs.size() < randomSongsCount) {
				try {
					Song song = (Song) songsForMood.get(
							rand.nextInt(songsForMood.size())).getObject();
					if (!songs.containsKey(song)) {
						int likeCount = 0;
						
						if(playedSongs.contains(song.getRadioMariaTag())) {
							likeCount = -4 + playedSongs.indexOf(song.getRadioMariaTag());
						}
						else {
							likeCount = getLikeCount(song);
						}
						songs.put(song, likeCount);
					}
				} catch (ClassNotFoundException e) {
					e.printStackTrace();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}

			Vector<Song> sortedSongs = new Vector<Song>();
			while (songs.size() > 0) {
				Iterator<Song> it = songs.keySet().iterator();
				Song bestSong = it.next();
				int highestLikeCount = songs.get(bestSong);
				while (it.hasNext()) {
					Song otherSong = it.next();
					int otherLikeCount = songs.get(otherSong);
					if (otherLikeCount > highestLikeCount) {
						bestSong = otherSong;
						highestLikeCount = otherLikeCount;
					}
				}
				sortedSongs.add(bestSong);
				songs.remove(bestSong);
				logMessage(sortedSongs.size() + ". " + bestSong.getSongTitle()
						+ " like count: " + highestLikeCount,
						LoggerInstance.RECOMMENDER);
			}
			Song song = null;
			try {
				song = playSong(sortedSongs);
				isActive = true;
			} catch (NoSongsFoundException e) {
				logMessage(
						"Could not play song because streaming host is not available",
						LoggerInstance.P2P);
				playBestSongForMood(mood);
			}

			if (playedSongs.size() == 3) {
				playedSongs.remove(0);
				playedSongs.add(song.getRadioMariaTag());
			} else {
				playedSongs.add(song.getRadioMariaTag());
			}

			model.addSongToSongsPlayed(song);
			return song;

		} else {
			throw new UnsupportedMoodException(mood);
		}
	}

	/**
	 * Tries songs to play songs in the vector starting from the head.
	 * 
	 * @param songs
	 * @return song that can be played
	 * @throws UnsupportedMoodException
	 * @throws IOException
	 * @throws NoSongsFoundException
	 */
	private Song playSong(Vector<Song> songs) throws UnsupportedMoodException,
			IOException, NoSongsFoundException {

		songs = sortPlayedSongs(songs);

		while (songs.size() > 0) {
			Song song = songs.get(0);
			if (audioPlayer.playSong(song.getUrl())) {
				return song;
			} else {
				logMessage("Peer unreachable: eliminate song and play new one",
						LoggerInstance.P2P);
				removeSong(song);
				songs.remove(song);
			}

		}
		throw new NoSongsFoundException();
	}

	/**
	 * Increments the like count for a certain song by one and returns the
	 * updated value
	 * 
	 * @param song
	 * @return like count after incrementing
	 */
	public int incrementAndGetLikeCount(Song song) {
		int previousLikeCount = getLikeCount(song);
		int currentLikeCount = previousLikeCount + 1;

		Number160 songTag = Number160.createHash(song.getRadioMariaTag());
		try {
			cs.setDomain(Number160.createHash("like"));
			LikeCounter counter = new LikeCounter(currentLikeCount);
			logMessage(
					"putting like count for song with tag: "
							+ song.getRadioMariaTag() + " new like count: "
							+ currentLikeCount, LoggerInstance.P2P);
			FutureDHT future = peer.put(songTag, new Data(counter), cs);
			future.awaitUninterruptibly();
		} catch (IOException e) {
			e.printStackTrace();
		}

		return currentLikeCount;

	}

	/**
	 * Retrieving the like count stored in the DHT for a specific song.
	 * 
	 * @param song
	 * @return
	 */
	public Integer getLikeCount(Song song) {
		Number160 songTag = Number160.createHash(song.getRadioMariaTag());
		logMessage(
				"getting like count for song with tag: "
						+ song.getRadioMariaTag(), LoggerInstance.P2P);
		getconfig.setDomain(Number160.createHash("like"));
		FutureDHT songsFuture = peer.get(songTag, getconfig);
		songsFuture.awaitUninterruptibly();

		if (songsFuture.isSuccess()) {
			LikeCounter counter = null;
			try {
				counter = (LikeCounter) songsFuture.getData().getObject();
			} catch (ClassNotFoundException e) {
				e.printStackTrace();
			} catch (IOException e) {
				e.printStackTrace();
			}
			return counter.getLikeCount();
		} else {
			return 0;
		}
	}

	/**
	 * Deletes the specified song from the DHT because the entry there is not up
	 * to date anymore.
	 * 
	 * @param song
	 * @throws IOException
	 */
	private void removeSong(Song song) throws IOException {
		Number160 genreHash = Number160.createHash(song.getGenre());
		Number160 contentKey = Number160.createHash(song.getUrl());
		Set<Number160> keyCollection = new HashSet<Number160>();
		keyCollection.add(contentKey);
		cr.setDomain(Number160.createHash("songs"));
		logMessage("removing song: " + song.getSongTitle()
				+ " from the DHT because it is unavailable", LoggerInstance.P2P);
		FutureDHT future = peer.remove(genreHash, keyCollection, cr);
		future.awaitUninterruptibly();
	}

	/**
	 * Plays a random song for a given genre
	 * 
	 * @param genre
	 * @return
	 */
	public Song playRandomSongForGenre(String genre) {
		Collection<Data> songs = getSongsForGenre(genre);
		if (songs != null) {
			try {
				Random rand = new Random();
				int songIndex = rand.nextInt(songs.size());
				Iterator<Data> iterator = songs.iterator();
				while (songIndex > 0) {
					iterator.next();
					songIndex--;
				}
				Song song = (Song) iterator.next().getObject();
				audioPlayer.playSong(song.getUrl());
				isActive = true;
				return song;

			} catch (ClassNotFoundException e) {
				e.printStackTrace();
			} catch (IOException e) {
				e.printStackTrace();
			}
			return null;
		} else {
			return null;
		}
	}

	/**
	 * Checks for replicas in the retrieved songs
	 * 
	 * @param song
	 * @return a vector of replicas
	 */
	public Vector<Song> getSongReplications(Song song) {
		Collection<Data> songs = getSongsForGenre(song.getGenre());
		Iterator<Data> iterator = songs.iterator();
		Vector<Song> replications = new Vector<Song>();
		if (songs != null) {
			try {
				while (iterator.hasNext()) {
					Song tempSong;
					tempSong = (Song) iterator.next().getObject();
					// Simple comparison based on radioMariaTag (see Song.java)
					if (tempSong.getRadioMariaTag().equalsIgnoreCase(
							song.getRadioMariaTag())) {
						replications.add(tempSong);
					}

				}
			} catch (ClassNotFoundException e) {
				e.printStackTrace();
				return replications;
			} catch (IOException e) {
				e.printStackTrace();
				return replications;
			}
		}
		return replications;
	}

	/**
	 * Audio player stop functionality
	 */
	public void stopSong() {
		audioPlayer.stop();
	}

	/**
	 * Audio player resume functionality
	 */
	public void resumeSong() {
		audioPlayer.resume();
	}

	public Boolean getIsActive() {
		return isActive;
	}

	/**
	 * Ads a controller listener to the audio player
	 * 
	 * @param controllerListener
	 */
	public void addControllerListenerToAudioPlayer(
			ControllerListener controllerListener) {
		((DefaultAudioPlayer) audioPlayer)
				.addControllerListener(controllerListener);
	}

	/**
	 * Sets the data model
	 * 
	 * @param dataModel
	 */
	public void addDataModel(DataModel dataModel) {
		this.model = dataModel;
	}

	/**
	 * Helper to write a string message to the specified logger message
	 * 
	 * @param message
	 * @param instance
	 */
	private void logMessage(String message, LoggerInstance instance) {
		model.addConsoleOutput(message, instance);
	}

	/**
	 * Gracefully disconnect from the network.
	 */
	public void leaveNetwork() {
	}

	private Vector<Song> sortPlayedSongs(Vector<Song> songs) {

		Vector<Song> sortedSongs = new Vector<Song>();
		Vector<Song> temporarySongs = new Vector<Song>();

		for (int i = 0; i < songs.size(); i++) {
			if (playedSongs.contains(songs.get(i).getRadioMariaTag())) {
				temporarySongs.add(songs.get(i));
			} else {
				sortedSongs.add(songs.get(i));
			}
		}

		sortedSongs.addAll(temporarySongs);

		return sortedSongs;
	}

}
